EC2 Instance Connect Endpoint で PrivateWithEgress に配置された EC2 Instance に接続する構成を AWS CDK で作成してみた
こんにちは、製造ビジネステクノロジー部の若槻です。
EC2 Instance Connect Endpoint を使用すると、踏み台ホストなどを使用せずにプライベートサブネット内の EC2 インスタンスに SSH や RDP 接続が可能となります。この機能は昨年 6 月に提供開始されました。
今回は、この EC2 Instance Connect Endpoint で PrivateWithEgress のサブネットに配置された EC2 Instance に SSH 接続可能とする構成のリソース一式を AWS CDK で作成してみました。
試してみた
CDK コード
リソース一式を作成する CDK コードは次のようになります。(2025/01/03 更新。セキュリティグループを Construct 分割して可読性を確保する改善を行った。)
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
/**
* セキュリティグループ用 Construct
*/
class SecurityGroupConstruct extends Construct {
public readonly forInstanceConnectEndpoint: ec2.SecurityGroup;
public readonly forEc2Instance: ec2.SecurityGroup;
constructor(scope: Construct, id: string, vpc: ec2.IVpc) {
super(scope, id);
// EC2 Instance Connect Endpoint 用セキュリティグループの作成
const forInstanceConnectEndpoint = new ec2.SecurityGroup(
this,
'ForInstanceConnectEndpoint',
{
vpc,
allowAllOutbound: false,
}
);
// EC2 Instance 用セキュリティグループの作成
const forEc2Instance = new ec2.SecurityGroup(this, 'ForEc2Instance', {
vpc,
allowAllOutbound: false,
});
// EC2 Instance Connect Endpoint から EC2 Instance への 22 ポートの通信を許可
forInstanceConnectEndpoint.connections.allowTo(
forEc2Instance,
ec2.Port.tcp(22)
);
// EC2 Instance から外部への通信は 443 ポートのみ許可
forEc2Instance.addEgressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(443));
this.forInstanceConnectEndpoint = forInstanceConnectEndpoint;
this.forEc2Instance = forEc2Instance;
}
}
export class MainStack extends cdk.Stack {
constructor(scope: Construct, id: string) {
super(scope, id);
/**
* VPC の作成
*/
const vpc = new ec2.Vpc(this, 'VPC', {
subnetConfiguration: [
// NAT Gateway を作成するパブリックサブネット
{
cidrMask: 24,
name: 'Public',
subnetType: ec2.SubnetType.PUBLIC,
},
// 接続先の EC2 Instance を作成するプライベートサブネット
// インスタンスから外部への通信を許可するために Egress ルートを追加
{
cidrMask: 24,
name: 'PrivateWithEgress',
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
// EC2 Instance Connect Endpoint を作成するプライベートサブネット
{
cidrMask: 24,
name: 'PrivateIsolated',
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
},
],
});
const securityGroups = new SecurityGroupConstruct(
this,
'SecurityGroups',
vpc
);
/**
* EC2 Instance Connect Endpoint を作成
*/
new ec2.CfnInstanceConnectEndpoint(this, 'InstanceConnectEndpoint', {
subnetId: vpc.selectSubnets({
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
}).subnetIds[0],
securityGroupIds: [
securityGroups.forInstanceConnectEndpoint.securityGroupId,
],
});
/**
* EC2 Instance を作成
*/
new ec2.Instance(this, 'Ec2Instance', {
vpc,
vpcSubnets: {
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
instanceType: new ec2.InstanceType('t3.micro'),
machineImage: new ec2.AmazonLinuxImage({
generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2023,
}),
securityGroup: securityGroups.forEc2Instance,
});
}
}
以前のコードはこちら
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
export class CdkSampleStack extends cdk.Stack {
constructor(scope: Construct, id: string) {
super(scope, id);
// VPC の作成
const vpc = new ec2.Vpc(this, 'VPC', {
subnetConfiguration: [
// NAT Gateway を作成するパブリックサブネット
{
cidrMask: 24,
name: 'Public',
subnetType: ec2.SubnetType.PUBLIC,
},
// 接続先の EC2 Instance を作成するプライベートサブネット
// インスタンスから外部への通信を許可するために Egress ルートを追加
{
cidrMask: 24,
name: 'PrivateWithEgress',
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
// EC2 Instance Connect Endpoint を作成するプライベートサブネット
{
cidrMask: 24,
name: 'PrivateIsolated',
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
},
],
});
/**
* EC2 Instance Connect Endpoint に関するリソースを作成
*/
// EC2 Instance Connect Endpoint に関連付けるセキュリティグループ
const instanceConnectEndpointSecurityGroup = new ec2.SecurityGroup(
this,
'InstanceConnectEndpointSecurityGroup',
{
vpc,
allowAllOutbound: false,
}
);
// EC2 Instance Connect Endpoint を作成
new ec2.CfnInstanceConnectEndpoint(this, 'InstanceConnectEndpoint', {
subnetId: vpc.selectSubnets({
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
}).subnetIds[0],
securityGroupIds: [instanceConnectEndpointSecurityGroup.securityGroupId],
});
/**
* EC2 Instance と EC2 Instance Connect Endpoint 間の通信を許可するためのルールを設定
*/
// EC2 Instance に関連付けるセキュリティグループ
const ec2InstanceSecurityGroup = new ec2.SecurityGroup(
this,
'Ec2InstanceSecurityGroup',
{
vpc,
allowAllOutbound: false,
}
);
// EC2 Instance Connect Endpoint から EC2 Instance への通信を許可するための Ingress ルール
ec2InstanceSecurityGroup.addIngressRule(
instanceConnectEndpointSecurityGroup,
ec2.Port.tcp(22)
);
// EC2 Instance Connect Endpoint から EC2 Instance への通信を許可するための Egress ルール
instanceConnectEndpointSecurityGroup.addEgressRule(
ec2InstanceSecurityGroup,
ec2.Port.tcp(22)
);
/**
* EC2 Instance および関連リソースを作成
*/
// EC2 Instance を作成
new ec2.Instance(this, 'Ec2Instance', {
vpc,
vpcSubnets: {
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
instanceType: new ec2.InstanceType('t3.micro'),
machineImage: new ec2.AmazonLinuxImage({
generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2023,
}),
securityGroup: ec2InstanceSecurityGroup,
});
// EC2 Instance から外部への通信は 443 ポートのみ許可
ec2InstanceSecurityGroup.addEgressRule(
ec2.Peer.anyIpv4(),
ec2.Port.tcp(443)
);
}
}
AWS CDK で EC2 Instance Connect Endpoint を構築する場合は、L1 Construct class である CfnInstanceConnectEndpoint が利用可能なので利用しています。
ちなみに以下の記事にある通り、EC2 Instance Connect Endpoint の提供開始直後は CfnInstanceConnectEndpoint が未提供だったため、AwsCustomResource を利用しての構築が必要だったようです。
デプロイ、動作確認
前述の CDK の実装をデプロイした様子です。(2025/01/03 更新)
以前のコードの実装をデプロイした様子はこちら
作成したEndpoint を使ってマネジメントコンソールから EC2 Instance Connect による接続を実施してみます。ここで選択されているEndpoint 名冒頭の eice
というのは「EC2 Instance Connect Endpoint」の略です。
インスタンスに SSH で接続できました。
インターネット経由で外部ツール(ここでは psql)のダウンロードおよびインストールもできることが確認できました。
[ec2-user@ip-10-0-2-220 ~]$ sudo dnf install postgresql15
Last metadata expiration check: 18:05:21 ago on Sat Sep 28 14:25:01 2024.
Dependencies resolved.
=================================================================================================================================================================
Package Architecture Version Repository Size
=================================================================================================================================================================
Installing:
postgresql15 x86_64 15.8-1.amzn2023.0.1 amazonlinux 1.6 M
Installing dependencies:
postgresql15-private-libs x86_64 15.8-1.amzn2023.0.1 amazonlinux 145 k
Transaction Summary
=================================================================================================================================================================
Install 2 Packages
Total download size: 1.8 M
Installed size: 6.9 M
Is this ok [y/N]: y
Downloading Packages:
(1/2): postgresql15-private-libs-15.8-1.amzn2023.0.1.x86_64.rpm 1.3 MB/s | 145 kB 00:00
(2/2): postgresql15-15.8-1.amzn2023.0.1.x86_64.rpm 11 MB/s | 1.6 MB 00:00
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
Total 7.9 MB/s | 1.8 MB 00:00
Running transaction check
Transaction check succeeded.
Running transaction test
Transaction test succeeded.
Running transaction
Preparing : 1/1
Installing : postgresql15-private-libs-15.8-1.amzn2023.0.1.x86_64 1/2
Installing : postgresql15-15.8-1.amzn2023.0.1.x86_64 2/2
Running scriptlet: postgresql15-15.8-1.amzn2023.0.1.x86_64 2/2
Verifying : postgresql15-15.8-1.amzn2023.0.1.x86_64 1/2
Verifying : postgresql15-private-libs-15.8-1.amzn2023.0.1.x86_64 2/2
Installed:
postgresql15-15.8-1.amzn2023.0.1.x86_64 postgresql15-private-libs-15.8-1.amzn2023.0.1.x86_64
Complete!
[ec2-user@ip-10-0-2-220 ~]$ psql --version
psql (PostgreSQL) 15.8
注意点
Endpoint のサブネットの更新などを行うと、複数作成エラーが発生する場合がある
EC2 Instance Connect Endpoint が配置されているサブネットの更新などを行うとEndpoint の再作成が行われますが、その際に CDK デプロイが次のようなエラーになる場合があります。
12:07:25 AM | UPDATE_FAILED | AWS::EC2::InstanceConnectEndpoint | InstanceConnectEndpoint
Resource handler returned message: "You've reached the quota for the maximum number of Instance Connect Endpoints for this subnet. Delete unused Instance Connect Endpoints, or request a quota increase. (Service: Ec
2, Status Code: 400, Request ID: 5d5d8149-4b42-4e62-a92d-ad065afe1211)" (RequestToken: f745bea4-6329-590c-a0b2-6c07319c9731, HandlerErrorCode: ServiceLimitExceeded)
これは各単位ごとのEndpoint の最大作成数は次のようになっており、今回だと VPC およびサブネットの最大作成数のクォータに抵触したためエラーとなっていました。
単位 | 最大作成数 |
---|---|
AWS アカウントごと | 5 |
VPC あたり | 1 |
サブネットあたり | 1 |
CfnInstanceConnectEndpoint は L1 Construct であり L2 Construct のようにリソース再作成のハンドリングを上手く行ってくれない場合があるため、注意するようにしましょう。
Endpoint を再作成したら前のEndpoint がしばらく選択可能になる?
EC2 Instance Connect Endpoint を CDK で再作成した後に、Endpoint を使用して EC2 Instance Connect を試そうとすると、再作成前と後のEndpoint がどちらも選択可能になるようになってしまいました。
これは実際にリソースが残っているわけではなくブラウザのキャッシュの影響によるもののようで、キャッシュを削除したら現在作成されているEndpoint のみが選択可能になりました。何回か再作成を繰り返していると選択可能なEndpoint が 3 つ、4 つと増えていったので少し戸惑いました。
おわりに
EC2 Instance Connect Endpoint で PrivateWithEgress に配置された EC2 Instance に接続する構成のリソース一式を AWS CDK で作成してみました。
まだ L1 Construct のみの提供ですが、設定項目は多くないため、再作成時の複数作成エラーのハンドリングを除くと、CDK での構築も比較的簡単に行えました。
以上